package session

import (
	"bytes"
	"fmt"
	"mi/compiler/ast"
	"mi/compiler/checker"
	"mi/compiler/scanner"
	"unicode/utf8"
)

type markdownBuilder struct {
	format    bool
	lineStart bool
	indent    int
	buffer    bytes.Buffer
	checker   checker.TypeChecker
}

const maxDocumentPrecedence = 9
func getDocumentPrecedence(node ast.Document) int {
	switch node.(type) {
	case *ast.AuthorTag:
		return 1
	case *ast.AliasTag:
		return 2
	case *ast.ParameterTag:
		return 3
	case *ast.ReturnTag:
		return 4
	case *ast.ExceptionTag:
		return 5
	case *ast.SeeTag:
		return 6
	case *ast.SinceTag:
		return 7
	case *ast.VersionTag:
		return 8
	case *ast.DeprecatedTag:
		return 9
	}

	return 0
}

// TODO: Need to format text to fit markdown
func documentConvertToMarkdown(documents []ast.Document, checker checker.TypeChecker) string {
	if len(documents) == 0 {
		return ""
	}

	var builder markdownBuilder
	builder.checker = checker
	for _, document := range documents {
		builder.writeNode(document, true)
	}
	return builder.buffer.String()
}

func (m *markdownBuilder) writeString(text string, format bool) {
	if m.lineStart {
		m.buffer.WriteString(getIndentString(m.indent))
		m.lineStart = false
	}

	if format {
		text = m.formatString(text)
	}

	m.buffer.WriteString(text)
}

func (m *markdownBuilder) writeLine() {
	m.buffer.WriteString("\t\t\n")
	m.lineStart = true
}

func (m *markdownBuilder) ensureAtNewLine() {
	if !m.lineStart {
		m.writeLine()
	}
}

func (m *markdownBuilder) formatString(text string) string {
	if len(text) == 0 {
		return ""
	}

	var buffer bytes.Buffer
	var pos = 0
	for pos < len(text) {
		ch, size := utf8.DecodeRuneInString(text[pos:])
		switch ch {
		case '\\':
			buffer.WriteString("\\\\")
		case '`':
			buffer.WriteString("\\`")
		case '*':
			buffer.WriteString("\\*")
		case '_':
			buffer.WriteString("\\_")
		case '{':
			buffer.WriteString("\\{")
		case '}':
			buffer.WriteString("\\{")
		case '[':
			buffer.WriteString("\\[")
		case ']':
			buffer.WriteString("\\]")
		case '(':
			buffer.WriteString("\\(")
		case ')':
			buffer.WriteString("\\)")
		case '#':
			buffer.WriteString("\\#")
		//case '+':
		//	buffer.WriteString("\\+")
		//case '-':
		//	buffer.WriteString("\\-")
		//case '.':
		//	buffer.WriteString("\\.")
		case '!':
			buffer.WriteString("\\!")
		case ' ':
			buffer.WriteString("&nbsp;")
		default:
			buffer.WriteRune(ch)
		}

		pos += size
	}
	return buffer.String()
}

func (m *markdownBuilder) writeNode(node ast.Node, format bool) {
	if isNull(node) {
		return
	}

	var saveFormat = m.format
	m.format = format

	switch n := node.(type) {
	case *ast.Number:
		m.writeNumberNode(n)
	case *ast.Version:
		m.writeVersion(n)
	case *ast.PlainText:
		m.writePlainText(n)
	case *ast.Header:
		m.writeHeader(n)
	case *ast.Bold:
		m.writeBold(n)
	case *ast.Italic:
		m.writeItalic(n)
	case *ast.BoldItalic:
		m.writeBoldItalic(n)
	case *ast.Paragraph:
		m.writeParagraph(n)
	case *ast.CodeText:
		m.writeCodeText(n)
	case *ast.CodeBlock:
		m.writeCodeBlock(n)
	case *ast.CodeLink:
		m.writeCodeLink(n)
	case *ast.Link:
		m.writeLink(n)
	case *ast.LineBreak:
		m.writeLineBreak(n)
	case *ast.OrderedList:
		m.writeOrderedList(n)
	case *ast.UnorderedList:
		m.writeUnorderedList(n)
	case *ast.ListItem:
		m.writeListItem(n)
	case *ast.AuthorTag:
		m.writeAuthorTag(n)
	case *ast.AliasTag:
		m.writeAliasTag(n)
	case *ast.ParameterTag:
		m.writeParameterTag(n)
	case *ast.ReturnTag:
		m.writeReturnTag(n)
	case *ast.ExceptionTag:
		m.writeExceptionTag(n)
	case *ast.SeeTag:
		m.writeSeeTag(n)
	case *ast.SinceTag:
		m.writeSinceTag(n)
	case *ast.VersionTag:
		m.writeVersionTag(n)
	case *ast.DeprecatedTag:
		m.writeDeprecatedTag(n)
	case *ast.DocRoot:
		m.writeDocRoot(n)
	default:
		panic(fmt.Sprintf("Unknown document kind(%T) in 'writeDocument'", node))
	}

	m.format = saveFormat
}

func (m *markdownBuilder) writeNumberNode(node *ast.Number) {
	m.writeString(node.Value, m.format)
}

func (m *markdownBuilder) writeVersion(node *ast.Version) {
	m.writeNode(node.Left, true)
	m.writeString(".", false)
	m.writeNode(node.Right, true)
}

func (m *markdownBuilder) writePlainText(node *ast.PlainText) {
	m.writeString(node.Value, m.format)
}

func (m *markdownBuilder) writeHeader(node *ast.Header) {
	m.ensureAtNewLine()
	m.writeString(node.Token.ToString(), false)
	m.writeString(" ", false)
	m.writeNode(node.Text, true)
	m.writeLine()
}

func (m *markdownBuilder) writeBold(node *ast.Bold) {
	m.buffer.WriteString("**")
	m.writeNode(node.Text, true)
	m.buffer.WriteString("**")
}

func (m *markdownBuilder) writeItalic(node *ast.Italic) {
	m.buffer.WriteString("*")
	m.writeNode(node.Text, true)
	m.buffer.WriteString("*")
}

func (m *markdownBuilder) writeBoldItalic(node *ast.BoldItalic) {
	m.buffer.WriteString("***")
	m.writeNode(node.Text, true)
	m.buffer.WriteString("***")
}

func (m *markdownBuilder) writeParagraph(node *ast.Paragraph) {
	for _, document := range node.Content.Array() {
		m.writeNode(document, true)
	}
}

func (m *markdownBuilder) writeCodeText(node *ast.CodeText) {
	m.writeString("`", false)
	m.writeNode(node.Content, false)
	m.writeString("`", false)
}

func (m *markdownBuilder) writeCodeBlock(node *ast.CodeBlock) {
	m.ensureAtNewLine()
	m.writeString("``` ", false)
	m.writeNode(node.Language, true)
	m.writeLine()
	for _, document := range node.Content.Array() {
		m.writeNode(document, false)
	}
	m.writeLine()
	m.writeString("```", false)
	m.writeLine()

}

func (m *markdownBuilder) writeDocumentCallExpression(node *ast.DocumentCallExpression) {

}

func (m *markdownBuilder) writeCodeLink(node *ast.CodeLink) {
	if isNull(node.Name) {
		m.writeString("[", false)
		m.writeString(getNodeText(node.Expression, nil), false)
		m.writeString("]", false)
		// TODO: Not implement
		m.writeString("(", false)
		m.writeString(m.getExpressionAddress(node.Expression), false)
		m.writeString(")", false)
	} else {
		m.writeString("[", false)
		m.writeNode(node.Name, false)
		m.writeString("]", false)
		m.writeString("(", false)
		m.writeString(m.getExpressionAddress(node.Expression), false)
		m.writeString(")", false)
	}
}

func (m *markdownBuilder) writeLink(node *ast.Link) {
	if isNull(node.Name) {
		m.writeString("[", false)
		m.writeNode(node.Name, false)
		m.writeString("]", false)
		// TODO: Not implement
		m.writeString("(www.www.com)", false)
	} else {
		m.writeString("[", false)
		m.writeNode(node.Name, false)
		m.writeString("]", false)
		m.writeString("(",false)
		m.writeNode(node.Address, false)
		m.writeString(")", false)
	}
}

func (m *markdownBuilder) writeLineBreak(node *ast.LineBreak) {
	m.writeLine()
}

func (m *markdownBuilder) writeOrderedList(node *ast.OrderedList) {
	var saveIndent = m.indent
	m.indent = node.Level
	for _, document := range node.Content.Array() {
		m.ensureAtNewLine()
		if ast.IsListItem(document) {
			m.writeString("1. ", false)
		}
		m.writeNode(document, true)
		m.writeLine()
	}
	m.indent = saveIndent
}

func (m *markdownBuilder) writeUnorderedList(node *ast.UnorderedList) {
	var saveIndent = m.indent
	m.indent = node.Level
	for _, document := range node.Content.Array() {
		m.ensureAtNewLine()
		if ast.IsListItem(document) {
			m.writeString("- ", false)
		}
		m.writeNode(document, true)
		m.writeLine()
	}
	m.indent = saveIndent
}

func (m *markdownBuilder) writeListItem(node *ast.ListItem) {
 	m.writeNode(node.Content, true)
}

func (m *markdownBuilder) writeAuthorTag(node *ast.AuthorTag) {
	m.writeString("\t- ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeAliasTag(node *ast.AliasTag) {
	m.writeString("\t- ", false)
	m.writeString(getNodeText(node.Name, nil), true)
	m.writeString(" ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeParameterTag(node *ast.ParameterTag) {
	m.writeString("\t- ", false)
	m.writeString("**", false)
	m.writeString(getNodeText(node.Name, nil), true)
	m.writeString("**", false)
	m.writeString(" ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeExceptionTag(node *ast.ExceptionTag) {
	m.writeString("\t- ", false)
	m.writeString("**", false)
	m.writeString(getNodeText(node.Type, nil), true)
	m.writeString("**", false)
	m.writeString(" ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeReturnTag(node *ast.ReturnTag) {
	m.writeString("\t- ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) getExpressionAddress(expr ast.Expression) string {
	var getDeclarationLocation = func(decl ast.Declaration) string {
		if !isNull(decl) {
			var file = ast.GetSourceFileOfNode(decl)
			var offset = scanner.SkipTrivia(file.Text, decl.Pos(), false, false)
			var location = scanner.GetFileLineAndCharacterFromPosition(file, offset)
			return fmt.Sprintf("%s#%d",  file.Uri, location.Line+1)
		}

		return ""
	}

	switch expr.(type) {
	case *ast.DocumentCallExpression:
		signature, candidates := m.checker.GetResolvedSignature(expr)
		if isNull(signature) && len(candidates) > 0 {
			signature = candidates[0]
		}
		if !isNull(signature) && !isNull(signature.Declaration) {
			return getDeclarationLocation(signature.Declaration)
		}

	default:
		var symbol = m.checker.GetSymbolAtLocation(expr)
		if symbol != nil && !isNull(symbol.ValueDeclaration) {
			return getDeclarationLocation(symbol.ValueDeclaration)
		}
	}

	return ""
}

func (m *markdownBuilder) writeSeeTag(node *ast.SeeTag) {
	m.writeString("\t- ", false)
	m.writeString("[", false)
	m.writeString(getNodeText(node.Expression, nil), true)
	m.writeString("]", false)

	m.writeString("(" + m.getExpressionAddress(node.Expression) + ")", false)
	m.writeString(" ", true)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeSinceTag(node *ast.SinceTag) {
	m.writeString("\t- ",false)
	m.writeNode(node.Version, true)
	m.writeString(" ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeVersionTag(node *ast.VersionTag) {
	m.writeString("\t- ", false)
	m.writeNode(node.Version, true)
	m.writeString(" ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeDeprecatedTag(node *ast.DeprecatedTag) {
	m.writeString("\t- ", false)
	m.writeNode(node.Content, true)
	m.writeLine()
}

func (m *markdownBuilder) writeDocRoot(node *ast.DocRoot) {
	if node.Children.Len() == 0 {
		return
	}

	var documents = make([]ast.Document, node.Children.Len())
	copy(documents, node.Children.Array())

	var headerMark = make(map[int]bool)
	// Only the tag needs the title
	headerMark[0] = true

	for i := 0; i <= maxDocumentPrecedence; i ++ {
		for _, document := range documents {
			var precedence = getDocumentPrecedence(document)
			if precedence != i {
				continue
			}

			if !headerMark[precedence] {
				m.ensureAtNewLine()
				switch document.(type) {
				case *ast.AuthorTag:
					m.writeString("- ", false)
					m.writeString(ast.AuthorTagStr, false)
					m.writeLine()
				case *ast.AliasTag:
					m.writeString("- ", false)
					m.writeString(ast.AliasTagStr, false)
					m.writeLine()
				case *ast.ParameterTag:
					m.writeString("- ", false)
					m.writeString(ast.ParameterTagStr, false)
					m.writeLine()
				case *ast.ReturnTag:
					m.writeString("- ", false)
					m.writeString(ast.ReturnTagStr, false)
					m.writeLine()
				case *ast.ExceptionTag:
					m.writeString("- ", false)
					m.writeString(ast.ExceptionTagStr, false)
					m.writeLine()
				case *ast.SeeTag:
					m.writeString("- ", false)
					m.writeString(ast.SeeTagStr, false)
					m.writeLine()
				case *ast.SinceTag:
					m.writeString("- ", false)
					m.writeString(ast.SinceTagStr, false)
					m.writeLine()
				case *ast.VersionTag:
					m.writeString("- ", false)
					m.writeString(ast.VersionTagStr, false)
					m.writeLine()
				case *ast.DeprecatedTag:
					m.writeString("- ", false)
					m.writeString(ast.DeprecatedTagStr, false)
					m.writeLine()
				}
				headerMark[precedence] = true
			}

			m.writeNode(document, true)
		}
	}
}